Skip to content

feat(cli): detect Gemini managed-agent sandbox in detectAgentRuntime#1294

Open
jrusso1020 wants to merge 4 commits into
mainfrom
feat/cli-detect-gemini-managed-agent
Open

feat(cli): detect Gemini managed-agent sandbox in detectAgentRuntime#1294
jrusso1020 wants to merge 4 commits into
mainfrom
feat/cli-detect-gemini-managed-agent

Conversation

@jrusso1020

@jrusso1020 jrusso1020 commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

What

Adds Gemini managed-agent sandbox detection to the CLI's existing agent-runtime telemetry. Returns "gemini_managed_agent" from detectAgentRuntime() when the CLI is invoked inside a Google Gemini managed-agent sandbox; null elsewhere.

Why

The CLI already classifies invocations from Claude Code, Cursor, Codex, Replit, Hermes, openclaw, Pi, GitHub Copilot Agent (the existing VENDOR_RULES array in agent_runtime.ts). A Gemini managed-agent template is now using the hyperframes CLI internally for local renders (the dual-mode work in heygen-com/hyperframes-gemini-agent#1), and the team wants the same adoption signal for that surface.

How

Two changes in packages/cli/src/telemetry/agent_runtime.ts:

  1. Add "gemini_managed_agent" to the AgentRuntime type union.
  2. Add isGeminiManagedAgent() — a filesystem-based detector that returns true when both signals are present:
    • existsSync('/.agents/AGENTS.md')the uniqueness anchor. Definitionally a managed-agent artifact (the platform injects it per-run; its mtime tracks the interaction). Nothing in the generic Google-Cloud-on-gVisor universe (Cloud Run gen2, GKE Sandbox, Fly.io) mounts /.agents/. This single check carries essentially all uniqueness.
    • isGVisor()a guard, not a second uniqueness signal. gVisor itself is shared with GKE Sandbox + Cloud Run gen2 — it does not discriminate Antigravity. Its job here is to rule out a stray user-created /.agents/AGENTS.md on a non-sandbox host.
  3. detectAgentRuntime() checks isGeminiManagedAgent() ahead of the env-var-only VENDOR_RULES loop, since the Gemini signal is filesystem + kernel, not env.

GEMINI_API_KEY is deliberately NOT keyed on — it's user-settable on any host. Other signals explicitly excluded with documented reasons: gVisor alone, Google Compute Engine DMI (entire GCP reports this), job cgroup (Google-internal but broadly present), the egress-proxy env cluster (any MITM container sets these), /.google/ base-image overlay.

Empirical verification

Two independent verification passes by gemini-agent (introspection-based — env, /proc/version, /proc/1/cgroup, /proc/net/tcp, ip route, ip link, ls -la /, DMI, PID-1):

Stability — 3 independent fresh sandbox spins (the original spike, b9db4e56, d59d6361) all show both signals present. Full 3-spin matrix posted as a comment on this PR.

Uniqueness — FS-root + cgroup + netns + DMI + PID-1 introspection of d59d6361 to discriminate Antigravity-unique markers from the broader Google-Cloud-on-gVisor universe. Verdict:

  • /.agents/AGENTS.md carries essentially all uniqueness (definitional managed-agent mount).
  • gVisor is shared with GKE Sandbox + Cloud Run gen2 — guard only, not a discriminator.
  • Several hoped-for stronger markers don't exist in the empirical environment: no antigravity/managed-agent in any cgroup path; no ANTIGRAVITY_* / MANAGED_AGENT_* env var; no :8081 listener in /proc/net/tcp (proxy is the veth /30 gateway, not a local socket); no /credentials or service-account path.

Stability ≠ uniqueness; both are required for a correct detection rule. The 3-spin work confirms the signals are reliably present; the uniqueness analysis confirms they discriminate Antigravity from neighboring gVisor surfaces.

Test plan

4 new vitest cases in agent_runtime.test.ts:

  • Positive: /.agents/AGENTS.md exists + kernel is 4.19.0-gvisorgemini_managed_agent
  • Negative: gVisor alone (no /.agents/) → null (generic gVisor surface fallthrough)
  • Negative: /.agents/AGENTS.md exists on a non-gVisor kernel → null (dev-box false-positive guard)
  • Precedence: Gemini signal wins over a coincident CLAUDECODE env var
  • All 24 tests in agent_runtime.test.ts pass under vitest run (the CI runner)
  • bun run lint + bun run format:check repo-wide clean
  • Commit signed (GitHub verified)
  • Cross-spin empirical verification (stability)
  • Uniqueness empirical verification
  • Documentation updated — detectAgentRuntime()'s docstring updated inline; no separate docs surface

🤖 Signed off by Jerrai (hyperframes specialist)

Add `gemini_managed_agent` to the AgentRuntime union and a dedicated
isGeminiManagedAgent() detector. Empirical signal pair (from live-sandbox
introspection by gemini-agent, env_id b9db4e56, 2026-06-09):

  existsSync('/.agents/AGENTS.md')  AND  isGVisor()

The conjunction is what makes the rule safe:

  - `/.agents/AGENTS.md` excludes generic gVisor surfaces (GKE Sandbox,
    Cloud Run gen2) that don't mount the managed-agent layout.
  - The gVisor kernel check excludes a dev box that happens to have a
    stray `/.agents/` directory.

Implementation notes:

  - Filesystem-based check runs ahead of the env-var-only VENDOR_RULES
    loop. VENDOR_RULES is documented as "Only checks for the EXISTENCE
    of well-known env vars — never reads their values"; the Gemini
    signal is filesystem + kernel, not env, so it gets a dedicated
    branch rather than shoehorning into the rule list.

  - GEMINI_API_KEY is deliberately NOT keyed on — it's user-settable on
    any host. The filesystem + kernel pair is the actually-distinctive
    signal.

  - Reuses the existing isGVisor() helper for the kernel half of the
    conjunction; no duplication.

Tests (4 new, vitest):

  - Positive: /.agents/AGENTS.md + 4.19.0-gvisor → gemini_managed_agent
  - Negative: gVisor alone (no /.agents/) → null (generic gVisor surface)
  - Negative: /.agents/AGENTS.md alone (no gVisor) → null (dev box false-positive guard)
  - Precedence: Gemini signal wins over a coincident CLAUDECODE env var

Empirical caveat: signal was gathered from a single sandbox. Re-confirming
across additional sandbox spins is a follow-up; the rule is conservative
enough (conjunction of two independent signals) that a single-spin
false-positive is unlikely, but a single-spin variance bug (e.g. some
sandbox flavors omitting one of the two markers) would surface as
under-detection rather than over-detection.

Source for signals: introspection write-up at
/tmp/gemini-sandbox-detection-signals.md (gemini-agent, 2026-06-09).
@jrusso1020

Copy link
Copy Markdown
Collaborator Author

Cross-spin confirmation for the detection rule — 3 independent fresh sandbox spins, all consistent with the conjunction this PR keys on:

Spin /.agents/AGENTS.md /proc/version
spike (original) present 4.19.0-gvisor
b9db4e56 (verification batch) present (+ skills/, workspace/) Linux version 4.19.0-gvisor
d59d6361 (heavy-render env) -rw-r--r-- … 4328 … /.agents/AGENTS.md gvisor

So the primary signal (exists('/.agents/AGENTS.md') AND isGVisor()) held on all three — the single-spin caveat in the PR body can be treated as closed for v1 adoption. — gemini-agent

jrusso1020 and others added 2 commits June 9, 2026 07:44
…ring vs guard)

gemini-agent's uniqueness analysis (FS-root + cgroup + netns + DMI + PID-1
introspection of env d59d6361, 2026-06-09) revealed the two signals are
NOT co-equal:

- /.agents/AGENTS.md is the uniqueness anchor — definitionally a
  managed-agent artifact, injected per-run by the platform, mtime
  tracks the interaction. Nothing in the generic Google-Cloud-on-gVisor
  universe (Cloud Run gen2, GKE Sandbox, Fly.io) mounts /.agents/.
- isGVisor() is a guard, not a second uniqueness signal. gVisor itself
  is shared with GKE Sandbox + Cloud Run gen2 — its real job here is
  ruling out a stray user-created /.agents/AGENTS.md on a non-sandbox
  host.

The original 3-spin work proved *stability* (signals consistent across
sandbox spins). This pass adds *uniqueness* — confirming the signals
discriminate Antigravity from the broader gVisor universe, not just
that they're reliably present. Stability ≠ uniqueness; both are
required for a correct detection rule.

Code unchanged (the AND-gate is sound). Docstring reframed so a future
reader doesn't mistake the conjunction for two independent uniqueness
signals. Also enumerated the markers NOT keyed on (with reasons), so
future contributors don't reach for them by naming inference.

Source: gemini-agent uniqueness analysis write-up.
…optional AGENTS.md

The detector keyed on existsSync('/.agents/AGENTS.md'), but Google's Managed
Agents docs are explicit that AGENTS.md is OPTIONAL: an agent may declare its
instructions inline via system_instruction in agent.yaml and ship no AGENTS.md
file ("system_instruction and AGENTS.md are additive; both apply when present").
The platform auto-discovers the agent under the /.agents/ directory; skills
mount at /.agents/skills/ and AGENTS.md at /.agents/AGENTS.md only when shipped.

Keying on the file generalized only to templates that happen to bundle an
AGENTS.md (like HeyGen's own gemini-agent and Thor's reference). A managed agent
defined with inline instructions or a skills-only definition was a silent
false-negative. All three prior verification spins used our own AGENTS.md-bearing
template, so the gap was never exercised.

Broaden to the /.agents/ directory mount (still gVisor-guarded — false-positive
surface is unchanged) so skills-only and inline-instruction agents are detected.
Adds a regression test for the skills-but-no-AGENTS.md case. Documents the one
residual gap (pure inline-only, no skills/no AGENTS.md) that needs an empirical
spin to confirm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jrusso1020

Copy link
Copy Markdown
Collaborator Author

Generalizability fix pushed (52d1947)

Re-examined whether the detection generalizes across all Gemini managed agents, not just our own template. It did not, as originally written.

Finding: the detector keyed on existsSync('/.agents/AGENTS.md'), but Google's Managed Agents docs are explicit that AGENTS.md is optional — an agent can declare its instructions inline via system_instruction in agent.yaml and ship no AGENTS.md file ("the system_instruction and AGENTS.md are additive; both apply when present"). The platform auto-discovers the agent under the /.agents/ directory; skills mount at /.agents/skills/, workspace at /workspace/, and AGENTS.md at /.agents/AGENTS.md only when shipped.

So the file-based check generalized only to templates that happen to bundle an AGENTS.md — like our own hyperframes-gemini-agent and Thor's reference. A managed agent defined with inline instructions, or a skills-only definition, was a silent false-negative. All three verification spins used our AGENTS.md-bearing template, so this gap was never exercised — stability across spins ≠ generalizability across agent definitions.

Fix: key on the /.agents/ directory mount instead of the AGENTS.md file. Still gVisor-guarded, so the false-positive surface is unchanged (/.agents/ at filesystem root doesn't exist on Cloud Run gen2 / GKE Sandbox / Fly.io). Added a regression test for the skills-but-no-AGENTS.md case.

Residual gap (documented in-code): an agent defined with only inline system_instruction — no skills, no AGENTS.md — may not materialize a /.agents/ mount at all. That tail can't be closed from the docs; it needs an empirical spin of such an agent to confirm. The common case (skills and/or AGENTS.md present) is now covered.

Sources: Building Managed Agents, Managed Agents Quickstart.

…ntime docs

Self-review follow-ups (no behavior change for real managed agents):

- isGeminiManagedAgent now requires statSync("/.agents").isDirectory() rather
  than existsSync("/.agents"), matching the documented "directory mount"
  contract. existsSync matched any entry (a stray file/symlink named /.agents),
  widening the gVisor-gated false-positive surface beyond what the comment
  claimed. Tests now mock statSync accordingly (and drop a dead /.agents/skills
  mock clause the code never read).
- system.ts: the agent_runtime doc comment hard-coded the vendor list and said
  "detected by env-var existence only" — both stale once a filesystem/kernel
  detector (gemini_managed_agent) exists. Point at the AgentRuntime union and
  note the filesystem-marker case instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jrusso1020 added a commit that referenced this pull request Jun 10, 2026
…li comment

Self-review follow-up: the gemini_cli rule's comment stated the managed-agent
/.agents/ mount "takes precedence ahead of this loop" as fact, but that
detector lives in a separate PR (#1294). Rephrased conditionally so this PR's
comment is accurate whether or not that detector is present. No code change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid work. The generalizability fix (52d1947, keying on /.agents/ directory instead of the optional AGENTS.md file) was the right call — the first-draft false-negative was a meaningful correctness gap, and catching it before merge is exactly what the empirical verification pass was for.

P2 — duplicate test (no blocking issue, worth knowing)

The second test case ("detects a skills-only managed agent (no AGENTS.md)") is functionally identical to test 1. mockAgentsDir() stubs statSync("/.agents") — which is all the new implementation checks. Since AGENTS.md is no longer read anywhere in isGeminiManagedAgent, the mock doesn't differentiate between "AGENTS.md present" and "skills-only with no AGENTS.md". Both tests run the same mock + same assertion. The test documents the intent of the directory-vs-file decision, but provides no additional coverage over test 1. Keep it for documentation value, just noting it's illustrative rather than a unique assertion.

P3 — documented residual gap

The catch { return false; } silently handles both ENOENT and EACCES — that's correct, but the comment only mentions those two. In practice, any I/O error (e.g. EIO on a sandbox with restricted filesystem access) would also silently return false, which is the right behavior for a telemetry detector. No change needed, just noting the comment slightly understates the scope.

Overall: the uniqueness-anchor-vs-guard split is well documented, the tests cover the meaningful negative paths (gVisor-only, /.agents/-only, precedence), and the inline coverage-gap annotation for the inline-instruction-only tail is honest. ✅ Approving.

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at the head of this PR. The uniqueness-anchor / guard split (/.agents/ directory mount as uniqueness, isGVisor() as guard) is the right architecture for this detection rule, and the empirical 3-spin verification plus the discriminator audit against the broader Google-Cloud-on-gVisor surface is exactly the discipline that prevents the next-quarter "this thing fires on Cloud Run gen2 too" cleanup. Nice work.

LGTM. A few observations, none gating.

Strengths

  • Uniqueness vs guard, explicit in the docstring. /.agents/ is the discriminator; isGVisor() is the false-positive shield against a stray dev-box /.agents/. The docstring at agent_runtime.ts:233-256 walks through this clearly, including the things deliberately NOT keyed on (gVisor alone, GCE DMI, the job cgroup, the egress-proxy env cluster, /workspace/, GEMINI_API_KEY). Future-you will not have to rediscover this from logs.
  • Keying on the directory, not the file. statSync("/.agents").isDirectory() over existsSync("/.agents/AGENTS.md") correctly handles the AGENTS.md-optional case (skills-only / inline-instruction agents). The PR description and the docstring both call this out — and the "skills-only managed agent (no AGENTS.md)" test (agent_runtime.test.ts:200-209) pins exactly that generalization. Cheaper to write the more-general check now than to chase the omission post-launch.
  • Test coverage matches the docstring's reasoning surface. Positive case, the skills-only generalization, the no-/.agents/ fall-through to env rules, the dev-box false-positive guard via non-gVisor kernel, and the env-rule precedence flip — all five cases land. The mock pattern (vi.doMock("node:fs", ...) overriding statSync for /.agents only, delegating elsewhere) is correct.
  • Filesystem detector runs BEFORE the env-var loop (agent_runtime.ts:151-155). Correct order — the filesystem signal is more specific than any env-var marker, and a managed agent that happens to also export CLAUDECODE=1 should still classify as the managed agent. The test at agent_runtime.test.ts:262-272 pins this.
  • system.ts SystemMeta docstring updated to reflect the new fs-based path. Easy to overlook; you didn't.
  • The "things deliberately NOT keyed on" list in the docstring is the most valuable part of this PR for long-term maintenance. Every rejection has a reason; future PRs that want to add Gemini-adjacent detection will know not to retry Google Compute Engine DMI / gVisor-alone / the egress-proxy cluster.

Observations (non-gating)

  • Known inline-instruction-only gap is documented. Line 244-246 of the docstring acknowledges that an agent with ONLY inline system_instruction (no skills, no AGENTS.md) may not materialize a /.agents/ mount. The common case is covered; the tail is acknowledged. Fine to land — empirical confirmation of the tail can come in a follow-up if/when it actually matters.
  • platform() !== "linux" short-circuit at line 268. Quick win — saves the statSync syscall on macOS/Windows where the path can't exist. Wise.
  • existsSync still imported but the new code uses statSync (agent_runtime.ts:1). existsSync is still in use by the older isGVisor() / isKVM() helpers (presumably) — fine, just noting the import widened, not replaced.
  • The "mtime tracks interaction" framing in the PR body isn't part of the actual check (which keys on directory existence only). Reading the PR description and the code together, it's clear that mtime was descriptive context about the mount semantics, not a contract you're relying on. No action — just observing the framing.
  • Cross-PR ordering with #1328. Confirmed: isGeminiManagedAgent() runs ahead of the env-var loop here, and #1328 only adds env-var rules. So a managed-agent sandbox that also exports GEMINI_CLI=1 will classify as gemini_managed_agent, not gemini_cli. Order-independent merge between the two PRs — neither blocks the other.

What I verified

  • Diff against main — 3 files, +195/-7.
  • /.agents/ directory mount + gVisor guard logic walked end-to-end against the 5 test cases.
  • The statSync mock pattern is vi.doMock (correct for ESM dynamic-import test isolation); each mockAgentsDir() call returns a fresh override for /.agents only, delegating everything else to the real fs.
  • system.ts docstring update aligns with the new fs-based path and doesn't break any prior contracts.
  • No regression to existing VENDOR_RULES-driven detection (claude_code/codex/cursor/etc.) — they still loop after the FS check.
  • No CI / lint state to flag at this SHA.

What I didn't verify

  • The "inline-instruction-only managed agent" empirical case — flagged in the docstring as a known coverage gap. Worth a follow-up spin if it becomes a real category.
  • Whether /proc/version parsing in isGVisor() matches what's actually in the live sandbox kernel string — assumed pre-existing function from this PR's scope.

Clean PR. Empirical methodology + explicit uniqueness/guard reasoning + tests-that-match-the-reasoning is the model for this kind of detection rule.

Review by Rames D Jusso

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants